Set AuthStyle to InParams for public PKCE OAuth clients#4150
Set AuthStyle to InParams for public PKCE OAuth clients#4150amirejaz merged 4 commits intostacklok:mainfrom
Conversation
When oauth2.Endpoint.AuthStyle is unset (zero value), Go's oauth2 library uses AuthStyleAutoDetect, which tries HTTP Basic Auth first. For public PKCE clients (token_endpoint_auth_method=none), this sends an Authorization header with an empty password. Spec-compliant servers reject this and consume the single-use authorization code, causing the retry with client_id in POST body to fail with invalid_grant. Set AuthStyleInParams explicitly in all three locations where oauth2.Endpoint is constructed without AuthStyle: - pkg/auth/oauth/flow.go (authorization code exchange) - pkg/auth/remote/handler.go (token refresh from cached tokens) - pkg/registry/auth/oauth_token_source.go (registry auth) Add regression test with a strict mock server that rejects Basic Auth for public clients. Without the fix: 2 requests (auto-detect probing). With the fix: exactly 1 request. Fixes stacklok#4149 Signed-off-by: Greg Katz <gkatz@indeed.com>
Signed-off-by: Greg Katz <gkatz@indeed.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4150 +/- ##
==========================================
- Coverage 69.13% 68.86% -0.27%
==========================================
Files 462 462
Lines 46554 46572 +18
==========================================
- Hits 32185 32073 -112
- Misses 11897 11910 +13
- Partials 2472 2589 +117 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
amirejaz
left a comment
There was a problem hiding this comment.
Thanks for the well-documented fix — the root cause analysis is accurate and the test structure is the right approach. Two issues need addressing before merge:
-
AuthStyleInParamsis hardcoded for confidential clients inflow.goandhandler.go. Both paths can have a non-emptyClientSecret(e.g., DCR-registered clients inhandler.go). ForcingInParamswill break any OAuth server that mandatesclient_secret_basic. The fix should be conditional on whetherClientSecretis empty. -
Data race in the regression test:
requestCountis accessed from two goroutines without synchronisation — the test goroutine reads it while thehttptest.Servergoroutine writes it.go test -racewill flag this.
The change in pkg/registry/auth/oauth_token_source.go looks correct — that path is always a public-client PKCE flow (no secret).
- Make AuthStyle conditional on ClientSecret: public clients (no secret) use AuthStyleInParams to avoid burning single-use auth codes on strict servers; confidential clients use AuthStyleAutoDetect so servers that mandate client_secret_basic are not broken - Fix data race in regression test: replace plain int requestCount with atomic.Int32 so the httptest.Server goroutine and test goroutine access it safely under -race Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Hey @gkatz2, great fix! I pushed a small follow-up commit to your branch to address the conditional AuthStyle for confidential clients and a data race in the test. |
Summary
ToolHive's OAuth PKCE token exchange fails against strict OAuth 2.1 servers (e.g., Datadog's
mcp.datadoghq.com) becauseoauth2.Endpointstructs default toAuthStyleAutoDetect, which probes with HTTP Basic Auth first and burns the single-use authorization code. See the linked issue for the full mechanism.AuthStyle: oauth2.AuthStyleInParamsin the three locations whereoauth2.Endpointis constructed without itFixes #4149
Type of change
Test plan
task test)task lint-fix)mcp.datadoghq.com)Changes
pkg/auth/oauth/flow.goAuthStyleInParamson endpoint inNewFlowpkg/auth/remote/handler.goAuthStyleInParamson endpoint intryRestoreFromCachedTokenspkg/registry/auth/oauth_token_source.goAuthStyleInParamson endpoint inbuildOAuth2Configpkg/auth/oauth/flow_test.goTestAuthStyleInParams_StrictPublicClientServerregression testDoes this introduce a user-facing change?
Yes. Remote MCP servers with strict OAuth 2.1 implementations that previously failed with
invalid_grantduring PKCE token exchange will now connect successfully. No configuration changes needed.Special notes for reviewers
Why
AuthStyleInParamsis safe:AuthStyleAutoDetecttries HTTP Basic Auth first. For public PKCE clients (the common case — ToolHive's DCR registers withtoken_endpoint_auth_method=none), this sends anAuthorization: Basic base64(client_id:)header with an empty password. Spec-compliant servers reject this and consume the single-use authorization code, making the retry fail withinvalid_grant.AuthStyleInParamssendsclient_idin the POST body instead, avoiding the problem entirely.For the less common case where users provide their own client credentials via
--remote-auth-client-id/--remote-auth-client-secret,AuthStyleInParamssends the secret in the POST body (client_secret_poststyle). This is accepted by the vast majority of OAuth servers. The only servers that would break are those that exclusively requireclient_secret_basicand reject POST body credentials — an uncommon configuration, and a much smaller risk than the auth-code-burning bug this fixes.Generated with Claude Code